/*
 * GlobalPlatformCardService contains methods for the communication with Jacacards.
 * GlobalPlatformCardService extends JSmexCardService and implements all its abstract methods.
 * Altough this method are not need because Global Platfrom cards don't a filesystem.
 *
 * Created on 31. Januar 2007, 09:28
 *
 *  This file is part of JSmex.
 *
 *  JSmex is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  JSmex is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Foobar; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package jsmex.cardservice;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Random;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import jsmex.JSmexTools;
import jsmex.function.SmartCardFile;
import opencard.core.terminal.CardTerminalException;
import opencard.core.terminal.CommandAPDU;
import opencard.core.terminal.ResponseAPDU;
import opencard.core.util.HexString;

/**
 *
 * @author Tobias Senger
 */
public class GlobalPlatformCardService extends JSmexCardService{
    
    private static CommandAPDU command = new CommandAPDU(50);
    private static ResponseAPDU response;
    private byte[] cardManagerAID = { (byte)0xA0, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x03, (byte)0x00, (byte)0x00, (byte)0x00};
    private boolean mutalAuthSuccess = false;
    
    
    /** Creates a new instance of GlobalPlatformCardService */
    public GlobalPlatformCardService() {
        super();
    }
    
    
    /**
     * This method selects the application with the AID in argument aid.
     * @param aid The AID of the card application
     * @return Returns 'true' if the aid was found uns selected. Otherwise 'false' will be returned.
     */
    private boolean selectAID(byte[] aid) {
        
        byte[] select_cmd= {(byte)0x00, (byte)0xA4, (byte)0x04, (byte)0x00, (byte)aid.length};
        
        command.setLength(0);
        command.append(select_cmd);
        command.append(aid);
        command.append((byte)0x00);
        
        try {
            response = sendCommandAPDU(command);
            if (response.sw() != 0x9000) {
                lt.info("called method selectAID(aid :"+ HexString.hexify(aid) +"). returns SW: "+HexString.hexify(response.sw1())+" "+HexString.hexify(response.sw2()),this);
                return false;
            }
        }
        
        catch (CardTerminalException cte) {
            lt.warning("method selectFID(byte[] aid) throws CardTerminalException.",this);
            return false;
        }
        lt.info("called method selectAID(aid:"+HexString.hexify(aid)+" )returns SW: "+HexString.hexify(response.sw1())+" "+HexString.hexify(response.sw2()),this);
        return true;
    }
    
    /**
     * Sends a Initialisation Update commando to the SmartCard
     *
     * @param p1 Key Version Number
     * @param p2 Key Identifier
     * @param hostChallenge A byte array with 8 random bytes
     * @return byte[] Array with data from Response <p>
     * <ul>
     *      <li>Bytes 01-10: Key diversificaton data to derive the card static keys</li>
     *      <li>Bytes 11-12: Key Version Number and Secure Channel Protocol</li>
     *      <li>Bytes 13-20: Card Challenge</li>
     *      <li>Bytes 21-28: Card Cryptogramm</li>
     * </ul>
     */
    private byte[] initUpdate(byte p1, byte p2, byte[] hostChallenge) {
        
        byte[] cmd = { (byte)0x80, (byte)0x50, p1, p2, (byte)0x08};
        response = null;
        
        command.setLength(0);
        command.append(cmd);
        command.append(hostChallenge);
        command.append((byte)0x00);
        
        try {
            response = sendCommandAPDU(command);
        } catch (CardTerminalException cte) {
            lt.warning("method initUpdate(byte p1, byte p2, byte[] hostChallenge) throws CardTerminalException",this);
        }
        lt.info("called method initUpdate(p1: "+HexString.hexify(p1)+", p2:"+HexString.hexify(p2)+", hostChallenge: "+HexString.hexify(hostChallenge)+")returns: "+
                HexString.hexify(response.sw1())+" "+HexString.hexify(response.sw2()),this);
        return response.data();
    }
    
    /**
     * This method generate a Cryptogram from the arguments challenge1 and challenge2 with help from the sessionkey.
     * It uses the Triple-DES algorithm in CBC mode.
     * @param challenge1 A challenge as byte-array
     * @param challenge2 A challenge as byte-array
     * @param sessionKey The Sessionkey for calculting the cryptogram.
     * @return The Cryptogram as byte-array
     */
    private byte[] calculateCryptogram(byte[] challenge1, byte[] challenge2, byte[] sessionKey)  {
        
        Cipher des;
        byte[] cryptogram = new byte[8];
        byte[] temp_cryptogram = null;
        byte[] padding = {(byte)0x80, (byte)0, (byte)0, (byte)0, (byte)0, (byte)0, (byte)0, (byte)0};
        byte[] d =  JSmexTools.mergeByteArray(JSmexTools.mergeByteArray(challenge1, challenge2),padding);
        
        byte[] key = new byte[sessionKey.length+8];
        System.arraycopy( sessionKey, 0, key, 0, sessionKey.length );
        System.arraycopy( sessionKey, 0, key, 16, 8);
        
        
        IvParameterSpec iv = new IvParameterSpec(new byte[]{(byte)0, (byte)0, (byte)0, (byte)0, (byte)0, (byte)0, (byte)0, (byte)0});
        SecretKeySpec skey =  new SecretKeySpec(key, "DESede" );
        
        try {
            des = Cipher.getInstance("DESede/CBC/NoPadding");
            des.init(Cipher.ENCRYPT_MODE, skey, iv);
            temp_cryptogram = des.doFinal(d);
        } catch (NoSuchAlgorithmException ex) {
            lt.warning("method calculateCryptogram(challenge1: "+HexString.hexify(challenge1)+", challenge2: "+HexString.hexify(challenge2)+" sessionKey: "+HexString.hexify(sessionKey)+") throws NoSuchAlgorithmException",this);
        } catch (NoSuchPaddingException ex) {
            lt.warning("method calculateCryptogram(challenge1: "+HexString.hexify(challenge1)+", challenge2: "+HexString.hexify(challenge2)+" sessionKey: "+HexString.hexify(sessionKey)+") throws NoSuchPaddingException",this);
        } catch (InvalidKeyException ex) {
            lt.warning("method calculateCryptogram(challenge1: "+HexString.hexify(challenge1)+", challenge2: "+HexString.hexify(challenge2)+" sessionKey: "+HexString.hexify(sessionKey)+") throws InvalidKeyException",this);
        } catch (InvalidAlgorithmParameterException ex) {
            lt.warning("method calculateCryptogram(challenge1: "+HexString.hexify(challenge1)+", challenge2: "+HexString.hexify(challenge2)+" sessionKey: "+HexString.hexify(sessionKey)+") throws InvalidAlgorithmParameterException",this);
        } catch (BadPaddingException ex) {
            lt.warning("method calculateCryptogram(challenge1: "+HexString.hexify(challenge1)+", challenge2: "+HexString.hexify(challenge2)+" sessionKey: "+HexString.hexify(sessionKey)+") throws BadPaddingException",this);
        } catch (IllegalBlockSizeException ex) {
            lt.warning("method calculateCryptogram(challenge1: "+HexString.hexify(challenge1)+", challenge2: "+HexString.hexify(challenge2)+" sessionKey: "+HexString.hexify(sessionKey)+") throws IllegalBlockSizeException",this);
        }
        
        System.arraycopy(temp_cryptogram, 16, cryptogram, 0, 8);
        lt.info("called method calculateCryptogram(challenge1: "+HexString.hexify(challenge1)+", challenge2: "+HexString.hexify(challenge2)+" sessionKey: "+HexString.hexify(sessionKey)+") returns: "+HexString.hexify(cryptogram),this);
        return cryptogram;
    }
    
    /**
     * This method calculate a sessionkey with help from staticKey and derivationData.
     * Its uses the Triple-DES algorithm in ECB mode to calculate the sessionkey.
     * @param staticKey The static key for the calculation.
     * @param derivationData The needed derivation data.
     * @return Returns a byte-array with the session key
     */
    private byte[] calculateSessionKey(byte[] staticKey, byte[] derivationData) {
        
        Cipher des;
        byte[] sessionKey = null;
        byte[] desKey = new byte[24];
        
        System.arraycopy( staticKey, 0, desKey, 0, staticKey.length );
        System.arraycopy( staticKey, 0, desKey, 16, 8);
        
        SecretKey skey = new SecretKeySpec(desKey, "DESede" );
        try {
            des = Cipher.getInstance("DESede/ECB/NoPadding");
            des.init(Cipher.ENCRYPT_MODE, skey);
            sessionKey = des.doFinal(derivationData);
        } catch (NoSuchAlgorithmException ex) {
            lt.warning("method calculateSessionKey(staticKey: "+HexString.hexify(staticKey)+", derivationData: "+HexString.hexify(derivationData)+") throws NoSuchAlgorithmException",this);
        } catch (NoSuchPaddingException ex) {
            lt.warning("method calculateSessionKey(staticKey: "+HexString.hexify(staticKey)+", derivationData: "+HexString.hexify(derivationData)+") throws NoSuchPaddingException",this);
        } catch (InvalidKeyException ex) {
            lt.warning("method calculateSessionKey(staticKey: "+HexString.hexify(staticKey)+", derivationData: "+HexString.hexify(derivationData)+") throws InvalidKeyException",this);
        } catch (BadPaddingException ex) {
            lt.warning("method calculateSessionKey(staticKey: "+HexString.hexify(staticKey)+", derivationData: "+HexString.hexify(derivationData)+") throws BadPaddingException",this);
        } catch (IllegalBlockSizeException ex) {
            lt.warning("method calculateSessionKey(staticKey: "+HexString.hexify(staticKey)+", derivationData: "+HexString.hexify(derivationData)+") throws IllegalBlockSizeException",this);
        }
        
        lt.info("called method calculateSessionKey(staticKey: "+HexString.hexify(staticKey)+", derivationData: "+HexString.hexify(derivationData)+") returns: "+HexString.hexify(sessionKey),this);
        return sessionKey;
        
    }
    
    /**
     * This method generates the derivation data from challenge1 and challenge2.
     * @param challenge1 A 8-byte challenge
     * @param challenge2 A 8-byte challenge
     * @return 16-byte derivation data
     */
    private byte[] calculateDerivationData(byte[] challenge1, byte[] challenge2) {
        
        byte[] derivationData = new byte[16];
        System.arraycopy(challenge1, 4, derivationData, 0, 4);
        System.arraycopy(challenge2, 0, derivationData, 4, 4);
        System.arraycopy(challenge1, 0, derivationData, 8, 4);
        System.arraycopy(challenge2, 4, derivationData, 12, 4);
        lt.info("called method calculateDerivationData(challenge1: "+HexString.hexify(challenge1)+", challenge2: "+HexString.hexify(challenge2)+") returns: "+HexString.hexify(derivationData),this);
        return derivationData;
    }
    
    /**
     * This method fills the byte-array data with one 0x80 value and several 0x00 values until
     * the the array length is a multiple from 8 bytes
     * @param data The byte-array to fill.
     * @return The filled byte-array.
     */
    private byte[] padByteArray(byte[] data) {
        
        int i=0;
        byte[] tempdata = new byte[data.length+8];
        
        for (i=0;i<data.length;i++) {
            tempdata[i] = data[i];
        }
        
        tempdata[i]=(byte)0x80;
        
        for (i=i+1;((i)%8)!=0;i++) {
            tempdata[i]=(byte)0;
        }
        
        byte[] filledArray = new byte[i];
        System.arraycopy(tempdata, 0, filledArray, 0, i);
        lt.info("called method padByteArray(data: "+HexString.hexify(data)+"). returns: "+HexString.hexify(filledArray),this);
        return filledArray;
    }
    
    /**
     * This methode generates a APDU with a MAC from a given APDU.
     * It uses the Triple-DES algorithm in CBC mode.
     * @param capdu The APDU from which the new APDU should be generated.
     * @param sessionKey The sessionkey is needed to calculate the APDU with MAC
     * @return The APDU with MAC.
     */
    private byte[] calculateAPDUwithMAC(byte[] capdu, byte[] sessionKey) {
        
        Cipher des;
        byte[] mac = new byte[8];
        byte[] temp_mac = null;
        byte[] tempapdu = new byte[capdu.length];
        System.arraycopy(capdu, 0, tempapdu, 0, capdu.length);
        
        tempapdu[0] = (byte)(tempapdu[0]|(byte)0x04);
        tempapdu[4] = (byte)(tempapdu[4]+(byte)0x08);
        
        byte[] filledAPDU = padByteArray(tempapdu);
        
        byte[] key = new byte[sessionKey.length+8];
        System.arraycopy( sessionKey, 0, key, 0, sessionKey.length );
        System.arraycopy( sessionKey, 0, key, 16, 8);
        
        IvParameterSpec iv = new IvParameterSpec(new byte[]{(byte)0, (byte)0, (byte)0, (byte)0, (byte)0, (byte)0, (byte)0, (byte)0});
        SecretKeySpec skey =  new SecretKeySpec(key, "DESede" );
        
        try {
            des = Cipher.getInstance("DESede/CBC/NoPadding");
            des.init(Cipher.ENCRYPT_MODE, skey, iv);
            temp_mac = des.doFinal(filledAPDU);
        } catch (NoSuchAlgorithmException ex) {
            lt.warning("method calculateAPDUwithMAC(capdu: "+HexString.hexify(capdu)+", sessionKey: "+HexString.hexify(sessionKey)+") throws NoSuchAlgorithmException.",this);
        } catch (NoSuchPaddingException ex) {
            lt.warning("method calculateAPDUwithMAC(capdu: "+HexString.hexify(capdu)+", sessionKey: "+HexString.hexify(sessionKey)+") thows NoSuchPaddingException.",this);
        } catch (InvalidKeyException ex) {
            lt.warning("method calculateAPDUwithMAC(capdu: "+HexString.hexify(capdu)+", sessionKey: "+HexString.hexify(sessionKey)+") throws InvalidKeyException.",this);
        } catch (InvalidAlgorithmParameterException ex) {
            lt.warning("method calculateAPDUwithMAC(capdu: "+HexString.hexify(capdu)+", sessionKey: "+HexString.hexify(sessionKey)+") throws InvalidAlgorithmParameterException.",this);
        } catch (BadPaddingException ex) {
            lt.warning("method calculateAPDUwithMAC(capdu: "+HexString.hexify(capdu)+", sessionKey: "+HexString.hexify(sessionKey)+") throws BadPaddingException.",this);
        } catch (IllegalBlockSizeException ex) {
            lt.warning("method calculateAPDUwithMAC(capdu: "+HexString.hexify(capdu)+", sessionKey: "+HexString.hexify(sessionKey)+") throws IllegalBlockSizeException.",this);
        }
        
        System.arraycopy(temp_mac, temp_mac.length-8, mac, 0, 8);
        byte[] awm = JSmexTools.mergeByteArray(tempapdu, mac);
        lt.info("called method calculateAPDUwithMAC(capdu: "+HexString.hexify(capdu)+", sessionKey: "+HexString.hexify(sessionKey)+") returns: "+HexString.hexify(awm),this);
        return awm;
    }
    
    /**
     * Performs a external authentication. Before a exernal authentication can performed initUpdate must be run.
     *
     * @param cardChallenge Byte[8] array with the card challenge which returns from the initialise Update command
     * @param hostChallenge Byte[8] array with 8 random bytes
     * @param S_MAC Byte[8] array contains the S_MAC
     * @return Returns true if external Authentication was successful. Else returns false.
     * @see initUpdate
     */
    private boolean extAuth(byte[] cardChallenge, byte[] appChallenge, byte[] sessionKey) {
        
        byte[] mac;
        byte[] apdu = {(byte)0x80,(byte)0x82,(byte)0,(byte)0,(byte)0x08};
        byte[] hostCryptogram = calculateCryptogram(cardChallenge, appChallenge, sessionKey);
        
        mac = calculateAPDUwithMAC(JSmexTools.mergeByteArray(apdu, hostCryptogram),sessionKey);
        
        response = null;
        
        command.setLength(0);
        command.append(mac);
        
        try {
            response = sendCommandAPDU(command);
        } catch (CardTerminalException cte) {
            lt.warning("method extAuth(cardChallenge: "+HexString.hexify(cardChallenge)+", hostChallenge: "+HexString.hexify(appChallenge)+", sessionKey: "+HexString.hexify(sessionKey)+") CardTerminalException",this);
        }
        
        if (response.sw()==0x9000) {
            lt.info("called method extAuth(cardChallenge: "+HexString.hexify(cardChallenge)+", hostChallenge: "+HexString.hexify(appChallenge)+", sessionKey: "+HexString.hexify(sessionKey)+") returns: 'true'",this);
            return true;
        } else {
            lt.info("method: extAuth(cardChallenge: "+HexString.hexify(cardChallenge)+", hostChallenge: "+HexString.hexify(appChallenge)+", sessionKey: "+HexString.hexify(sessionKey)+") returns: 'false'",this);
            return false;
        }
    }
    
    
    
    /**
     * This method perfoms a mutual authentication with help of the arguments s_snc and s_mac.
     *
     * @param s_enc The static key S-ENC.
     * @param s_mac The static key S-MAC.
     * @return Returns true if the authentication was succesful. Otherwise it returns false.
     */
    public boolean mutualAuthentication(byte[] s_enc, byte[] s_mac) {
        
        if (mutalAuthSuccess) return true;
        
        byte[] appChallenge = new byte[8];
        byte[] cardChallenge = new byte[8];
        byte[] cardCryptogram = new byte[8];
        byte[] s_encSessionKey;
        byte[] s_macSessionKey;
        byte[] derivationData;
        Random r = new Random();
        
        r.nextBytes(appChallenge); // fill hostChallange with random bytes
        
        byte[] initResponse = initUpdate((byte)0,(byte)0, appChallenge);
        System.arraycopy( initResponse, 12, cardChallenge, 0, cardChallenge.length ); //extract cardChallenge
        System.arraycopy( initResponse, 20, cardCryptogram, 0, cardCryptogram.length); // extract cardCryptogram
        
        derivationData = calculateDerivationData(cardChallenge, appChallenge);
        s_encSessionKey = calculateSessionKey(s_enc, derivationData);
        
        byte[] calcCCryptogram = calculateCryptogram(appChallenge, cardChallenge, s_encSessionKey);
        
        if (!java.util.Arrays.equals(calcCCryptogram,cardCryptogram)) {
            lt.info("called method mutualAuthentication(s_enc: "+HexString.hexify(s_enc)+", s_mac: "+HexString.hexify(s_mac)+" )returns: 'false' (card authentication failed)",this);
            return false;
        }
        
        s_macSessionKey = calculateSessionKey(s_mac, derivationData);
        boolean success = extAuth(cardChallenge, appChallenge,s_macSessionKey);
        
        if (success) {
            lt.info("called method mutualAuthentication(s_enc: "+HexString.hexify(s_enc)+", s_mac: "+HexString.hexify(s_mac)+") returns: 'true'",this);
            mutalAuthSuccess = true;
        } else lt.info("called method mutualAuthentication(s_enc: "+HexString.hexify(s_enc)+", s_mac: "+HexString.hexify(s_mac)+") returns: 'false' (extern authentication failed)",this);
        
        return success;
    }
    
    /**
     * This method calls the GET STATUS command on the card. This command returns the applications on the card.
     * @param p1 This parameter defines which information will be returned. (see Global Platform specification)
     * @return A byte-array which contains the answer from the GET STATUS command
     */
    public byte[] getStatus(byte p1) {
        byte[] status_cmd= {(byte)0x80, (byte)0xF2, p1, (byte)0x00, (byte)0x02, (byte)0x4F, (byte)0x00, (byte)0x00};
        
        command.setLength(0);
        command.append(status_cmd);
        
        try {
            response = sendCommandAPDU(command);
        }
        
        catch (CardTerminalException cte) {
            lt.warning("method getStatus(byte p1) throws a CardTerminalException.",this);
            return null;
        }
        
        lt.info("called method getStatus(byte p1). returns SW: "+ HexString.hexify(response.sw1())+" "+HexString.hexify(response.sw1()),this);
        return response.data();
    }
    
    /**
     * Not used.
     * @param scfile
     * @return null
     */
    public SmartCardFile[] getFileList(SmartCardFile scfile) {
        return null;
    }
    
    /**
     * not used.
     * @param fid
     * @return null
     */
    public ResponseAPDU selectEF(byte[] fid) {
        return null;
    }
    
    /**
     * not used.
     * @param fid
     * @return null
     */
    public ResponseAPDU selectDF(byte[] fid) {
        return null;
    }
    
    /**
     * not used
     * @param oh
     * @param ol
     * @param length
     * @return null
     */
    public ResponseAPDU readBinary(byte oh, byte ol, byte length) {
        return null;
    }
    
    /**
     * not used
     * @param recno
     * @param length
     * @return null
     */
    public ResponseAPDU readRecord(byte recno, int length) {
        return null;
    }
    
    /**
     * not used
     * @return false
     */
    public boolean selectMF() {
        return false;
    }
    
    /**
     * not used
     * @param chvindication
     * @param chv
     * @return null
     */
    public ResponseAPDU verifyCHV(byte chvindication, String chv) {
        return null;
    }
    
    /**
     * not used
     * @param chvindicator
     * @return false
     */
    public boolean chvIsVerified(int chvindicator) {
        return false;
    }
    
    /**
     * not used
     * @param fidString
     * @return null
     */
    public SmartCardFile getSmartCardFile(String fidString) {
        return null;
    }
    
    /**
     * not used
     * @param oh
     * @param ol
     * @param data
     * @return null
     */
    public ResponseAPDU updateBinary(byte oh, byte ol, byte[] data) {
        return null;
    }
    
    /**
     * not used
     * @param recno
     * @param data
     * @return null
     */
    public ResponseAPDU updateRecord(byte recno, byte[] data) {
        return null;
    }
    
}
